#! /usr/bin/env python
# -*- coding: utf-8 -*-

#Author: João S. O. Bueno
# Copyright (c) João S. O. Bueno 2007, 2008, 2009

#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.


# TODO:

# aumentar o vocabulário e sortear lista de palavras
#implementar palavras escondidas ao contrário (direita p/ esquerda)
# usabilidade para poder reiniciar o jogo e configurações
# efeitos sonoros
# i18n
#documentar
# versão 1.0 está pronta

from random import choice, randrange
import pygame
from math import *

__version__ = 0.7

WIDTH, HEIGHT = 800,600
DWIDTH = WIDTH * 0.7

ALLOW_DIAGONALS = True
ALLOW_REVERSED = True

FOUND_WORD_COLOR = (0, 255, 83)
FONT_NAME = "VeraSeeBd.ttf"

BG_GRADIENT_START = (0, 96, 255)
BG_GRADIENT_END = (0, 0, 200)

#words = [
#"lixo", "girafa", "elefante", "tomate", "bacalhau", 
#"papainoel", "garrafa", "televisao", "fogao", "vinho", "azeitona", "gato", "cachorro"]

#size = 13



words= ["barata", "banana", "batata", "arvore", "banco", "onibus", "pato"]
size = 8

#words = ["gato", "sapo", "pato", "saco", "coca", "rato", "topa", "toga", "caco", "toca"]
#size = 10


class AlreadyUsed(Exception):
    pass

class TooManyWords(Exception):
    pass

class SmartList(list):
    """
    Extends a list functionality, adding some usefull methods
    """
    def is_aritmetic_progression(self, step = 1):
        """
        Verify if the elements of a list are evenly spaced. 
        Intended to be used with integer numbers, but might make sens
        with other types of elements 
        """
        if len(self) == 0:
            return True
        el = self[0]
        for i in range(1, len(self)):
            if self[i] - el != step:
                return False
            el = self[i]
        return True
    def is_same_value(self):
        """
        Verify if all elements in alist are equal
        """
        if len(self) == 0:
            return True
        v = self[0]
        for element in self:
            if element != v:
                return False
        return True

class Diagram(object):
    def __init__(self, words, size):
        self.size = size
        self.words = words
        respostas = {}
        letters = set()
        for word in words:
            letters.update(word)
        diagram = []
        for y in range(size):
            diagram.append ([])
            for x in range(size):
                diagram[-1].append(choice(list(letters)))
        self.used = set()
        self.diagram = diagram
        for word in words:
            ok = False
            count = 0
            while not ok:
                if ALLOW_REVERSED and choice(("f", "r")) == "r":
                    rword = list(reversed(word))
                else:
                    rword = word
                self.used_here = set()
                try:
                    direction = choice(("h", "v", "du", "dd"))
                    if direction == "h":
                       self.insert_horz_word(rword)
                    elif direction == "v":
                       self.insert_vert_word(rword) 
                    elif direction == "du":
                        self.insert_diagonal_up_word(rword)
                    elif direction == "dd":
                        self.insert_diagonal_down_word(rword)
                        
                            
                    ok = True
                    respostas[word] = self.used_here
                    self.used.update(self.used_here)
                except AlreadyUsed:
                    #used.symmetric_difference_update(used_here)
                    ok = False
                    count += 1
                    if count > 20 * size:
                        raise TooManyWords("Muitas palavras - pouco espaço!")
                    
        
        self.answers = respostas
        self.selected = set()
    
    def insert_horz_word(self, word):
        row = randrange(size)
        column = randrange(size - len(word))
        return self._insert_word(row, column, word, lambda x, y: x + y, lambda x, y: x)       
    
    def insert_vert_word(self, word):
        row = randrange(size - len(word))
        column = randrange(size)
        return self._insert_word(row, column, word, lambda x, y: x, lambda x, y: x + y)
           
    def insert_diagonal_up_word(self, word):
        column = randrange(size - len(word))
        row = randrange(len(word), size)
        return self._insert_word(row, column, word, lambda x, y: x + y, lambda x, y: x - y)
    def insert_diagonal_down_word(self, word):
        row = randrange(size - len(word))
        column = randrange(size - len(word))
        return self._insert_word(row, column, word, lambda x, y: x + y, lambda x, y: x + y)
    
    def _insert_word(self,row, column, word, horz_op, vert_op):
        diagram = self.diagram
        for i, letter in enumerate(word):
            x = horz_op(column, i)
            y = vert_op(row, i)
            if (x, y) in self.used and diagram[x][y] != letter:
                raise AlreadyUsed
        
            diagram[x][y] = letter
            self.used_here.add((x, y))
    
    def check_selection_inline(self):
        xs, ys = SmartList(), SmartList()
        for pos in self.selected:
            xs.append(pos[0])
            ys.append(pos[1])
        xs.sort()
        ys.sort()
        
        if ((xs.is_same_value() and ys.is_aritmetic_progression()) or
            (ys.is_same_value() and xs.is_aritmetic_progression()) or
            (xs.is_aritmetic_progression() and ys.is_aritmetic_progression())):
            return True
        else:
            return False 
    
    def check_selection_match_word(self):
        if not self.check_selection_inline():
            return False
        for answer in self.answers:
            if self.selected == self.answers[answer]:
                return answer
        return False
            
        
            
        
        
class GraphicDiagram(Diagram):
    def init_graphics(self):
        pygame.init()
        pygame.display.init()
        self.canvas = pygame.display.set_mode((WIDTH, HEIGHT))
        pygame.display.set_caption("Caça Palavras")
    #    pygame.time.delay(40)
    
    def __init__(self, *args):
        
        Diagram.__init__(self, *args)
        self.init_graphics()
        
        font_filename = FONT_NAME
        self.font_size = int (min(DWIDTH, HEIGHT) // size * 0.75)
        self.font_color = (255, 255, 255)
        self.font_highlight = (255, 255, 0)
        self.font = pygame.font.Font (font_filename, self.font_size)
        self.list_font_size = int(self.font_size / 1.5)
        self.list_font = pygame.font.Font (font_filename, self.list_font_size)
        self.offset = ( WIDTH - DWIDTH + (DWIDTH - min(DWIDTH, HEIGHT)) // 2 ,
                       (HEIGHT - min(DWIDTH, HEIGHT) ) // 2)  
        
        self.pix_size = min(DWIDTH, HEIGHT) // self.size
        self.line = [None, None]
        self.part_of_the_solution = set()
        self.answers_to_be_found = self.words[:]
        self.solution_lines = []
        self._build_display_list()
            
    def get_letter_at_px_pos(self , pos, accept_corners=False, close_factor = 0.35):
        px = pos[0] - self.offset[0]
        py = pos[1] - self.offset[1]
        x = px // self.pix_size
        y = py // self.pix_size
        if not 0 <= x < self.size or not 0 <= y < self.size:
            return None
        mod_x = px - x * self.pix_size
        mod_y = py - y * self.pix_size
        
        close_down = self.pix_size * close_factor
        close_up = self.pix_size * (1 - close_factor)
        
        #do not consider a hist if we are close to the diagonal intersections:
        if ((mod_x < close_down  or mod_x > close_up) and   
            (mod_y < close_down  or mod_y > close_up) and
            not accept_corners) :
            return None
        return x, y
    
    def loop(self):
        quit = False
        last_tick = -1
        ticks = 0
        has_changed = True
        self.selected = set()
        #self.selected_letters = []
        while not quit:
        
            #pygame.event.pump()
            pygame.event.pump()
            e = pygame.event.poll()
            if e.type == pygame.NOEVENT:
                pygame.time.delay(20)
                ticks += 1
            elif e.type == pygame.KEYDOWN:
                if e.key  == pygame.K_ESCAPE:
                    quit = True
                    break
            elif e.type == pygame.MOUSEBUTTONDOWN:
                if e.button == 1:
                    pos = self.get_letter_at_px_pos(e.pos, accept_corners=True)
                    if pos is not None:
                        self.selected.add(pos)
                        last_pos = first_pos = pos
                        self.line[0] = self.get_px_pos(pos)
                        self.line[1] = None
                elif e.button == 3:
                    self.selected = set()
                has_changed = True
            elif e.type == pygame.MOUSEBUTTONUP:
                self.has_changed = True
                if e.button == 1:
                    word = self.check_selection_match_word()
                    if word and word in self.answers_to_be_found:
                        self.answers_to_be_found.remove(word)
                        self.part_of_the_solution.update(self.selected)
                        self.solution_lines.append((self.line[0], self.get_px_pos(last_pos)))
                        self.selected = set()
                        has_changed = True
                        ticks += 1
                        
                        if len (self.answers_to_be_found) == 0:
                            #FIXME: create a  Game Over - you win / play again dialog
                            print "Voce ganhou"
                self.selected = set()        
                self.line = [None, None]
                    
            elif e.type == pygame.MOUSEMOTION:
                if e.buttons[0]:
                    pos = self.get_letter_at_px_pos(e.pos)
                    if pos is not None:
                        last_pos = pos
                        if pos not in self.selected:
                            self.selected.add (pos)
                    has_changed = True
                    self.line[1] = e.pos
            
            if has_changed and ticks != last_tick:
                last_tick = ticks
                if self.selected and not self.check_selection_inline():
                    self.selected = set((first_pos,))
                    #self.line[0] = self.get_px_pos(last_pos)
                    #self.line[1] = None
                elif self.selected:
                    new_selection = self.verify_selection()
                    if new_selection and len(new_selection) > 1:
                        self.selected = new_selection
                    
                 
                 
                self._paint_bg()
                self._draw_word_list()
                self._draw_solution_lines()
                self._draw_line()
                self._update_graph_diagram()
                pygame.display.flip()
                has_changed = False
        pygame.quit()
        
    def line_length (self, pos1, pos2):
        return sqrt ((pos2[0] - pos1[0]) ** 2 + (pos2[1] - pos1[1]) ** 2)
    
    def verify_selection(self):
        if self.line[0] is None or self.line[1] is None:
            return
        angle = atan2 ( - (self.line[1][1] - self.line[0][1]), self.line[1][0] - self.line[0][0]) #+ pi/2
        selection = set()
        x0, y0 = x, y = self.line[0]
        orig_length = self.line_length(self.line[0], self.line[1])
        while self.line_length((x0,y0), (x, y)) < orig_length:
            position = self.get_letter_at_px_pos((x,y), close_factor = -.25)
            if position is not None:
                selection.add(position)
            x += self.pix_size / 4.0 * cos(angle)
            y -= self.pix_size /4.0 * sin(angle)
        return selection
        
        
    def _update_graph_diagram(self):
        for y, row in enumerate(self.diagram):
            for x, letter in enumerate(row):
                self._draw_letter(x, y, (x,y) in self.selected)
    
    def _draw_letter(self, x, y, highlight=False):
        letter_size = self.pix_size // self.size
        letter = self.diagram[x][y].upper()
        if highlight:
            color = self.font_highlight
        elif (x,y) in self.part_of_the_solution:
            color = FOUND_WORD_COLOR
        else:
            color = self.font_color
            
        l_surf = self.font.render(letter, True, color)
        self.canvas.blit(l_surf, (self.pix_size * x + self.offset[0] + (self.pix_size - l_surf.get_width()) / 2 + 1, 
                                  self.pix_size * y + self.offset[1] + (self.pix_size - l_surf.get_height()) / 2))
    
    def _draw_solution_lines(self):
        if self.solution_lines:
            for line in self.solution_lines:
                self._draw_line(line, True)
        
    def _draw_line(self, line = None, solution = False):
        if line is None:
            line = self.line
        line_color = (255, 128, 0)
        if solution:
            line_color = FOUND_WORD_COLOR
        px_width = 4
        
        if line[0] is None or line[1] is None:
            return
        width = self.pix_size * 0.85 / 2.0
        pi2 = pi / 2 #pi == math.pi
        pi43 = 3 * pi2
        angle = atan2(line[1][0] - line[0][0], line[1][1] - line[0][1])
        angle += pi2
        
        side1, side2 = line[0], line[1]
        
        pos1 = (int(side1[0] + width * cos(angle + pi2)),
                 int(side1[1] - width * sin(angle + pi2)))
        pos2 = (int(side2[0] + width * cos(angle + pi2)),
                int(side2[1] - width * sin(angle + pi2)))
        pos3 = (int(side1[0] + width * cos(angle + pi43)),
                int(side1[1] - width * sin(angle + pi43)))
        pos4 = (int(side2[0] + width * cos(angle + pi43)),
                int(side2[1] - width * sin(angle + pi43)))
        
        pygame.draw.line(self.canvas, line_color, pos1, pos2, px_width)
        pygame.draw.line(self.canvas, line_color, pos3, pos4, px_width)
        
        x1 = min(pos1[0], pos3[0])
        y1 = min(pos1[1], pos3[1])
        x2 = max(pos1[0], pos3[0])
        y2 = max(pos1[1], pos3[1])
        
        x3 = min(pos2[0], pos4[0])
        y3 = min(pos2[1], pos4[1])
        x4 = max(pos2[0], pos4[0])
        y4 = max(pos2[1], pos4[1])
        
        w = width * 2 + px_width
        left_arc = pygame.surface.Surface((w, w),pygame.SRCALPHA, 32)

        del w
            
        pygame.draw.arc(left_arc, line_color, left_arc.get_rect(), pi / 2 , 3 * pi / 2, px_width)
        
        r_left_arc = pygame.transform.rotate(left_arc, angle / pi * 180)
        r_right_arc = pygame.transform.rotate(left_arc, angle / pi * 180 + 180)
        
        self.canvas.blit(r_right_arc, (line[0][0] - r_left_arc.get_rect()[2] / 2, line[0][1] - r_left_arc.get_rect()[3] / 2 ))
        self.canvas.blit(r_left_arc, (line[1][0] - r_right_arc.get_rect()[2] / 2, line[1][1] - r_right_arc.get_rect()[3] / 2 ))
        
    def _paint_bg(self):
        first_bg_color = BG_GRADIENT_START
        second_bg_color = BG_GRADIENT_END
        num_rects = 80
        rect_height = self.canvas.get_height() / float(num_rects)
        for i in range(num_rects):
            color = tuple([int(first_bg_color[j] / num_rects * (num_rects - i) +
                          second_bg_color[j] / num_rects *  i) for j in (0,1,2) ])
            pygame.draw.rect(self.canvas, 
                             color, 
                                (0, int(i * rect_height), 
                                 self.canvas.get_width(),
                                 rect_height + 1)
                            )
                            
    def _draw_word_list(self):
        width = WIDTH - DWIDTH
        height = HEIGHT
        #rect = (0, 0, WIDTH - DWIDTH, HEIGHT)
        
        color = (255,255,255)
        strike_color = (255, 0, 0)
        
        text_list = []
        for l_list in self.display_list:
            for word in l_list:
                text_list.append((word, self.list_font.render(word, True, color)))
            text_list.append(("", None ))
        v_size = 0
        for entry in text_list:
            if entry[0]:
                v_size += entry[1].get_height()
            else:
                v_size += self.list_font_size
        dy = height //2 - v_size // 2
        
        for entry in text_list:
            if entry[0]:
                
                self.canvas.blit(entry[1],
                                 ((width - entry[1].get_width()) // 2,
                                  dy))
                if entry[0] not in self.answers_to_be_found:
                    pygame.draw.line(self.canvas, strike_color,
                                     ((width // 2- entry[1].get_width() // 2) - self.list_font_size , dy),
                                     (width // 2 + entry[1].get_width() // 2 +  self.list_font_size,  dy + entry[1].get_height()),
                                     3)  
                dy += entry[1].get_height()
                                 
            else:
                dy += self.list_font_size
                
        
    
    def _build_display_list(self):
        #import pdb; pdb.set_trace()
        final_list = [[]]
        words = self.words[:]
        length = 1
        while words:
            
            for word in words[:]:
                if len(word) != length:
                    continue
                final_list[-1].append(word)
                words.remove(word)
            length += 1
            if final_list[-1]:
                final_list[-1].sort()
                final_list.append([])
        self.display_list = final_list[:-1]
        
    def get_px_pos (self, pos):
        px = int ((pos[0]  + 0.5) * self.pix_size) + self.offset[0]
        py = int ((pos[1]  + 0.5) * self.pix_size) + self.offset[1]
        return px, py
    
if __name__ == "__main__":
    d = GraphicDiagram(words, size)
    d.loop()
